package orchestration.xml;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import orchestration.android.parcelable.ParcelableArtifact;
import orchestration.businessprocess.model.BPArtifact;
import orchestration.businessprocess.model.BPArtifactId;
import orchestration.businessprocess.model.BPMImpl;
import orchestration.businessprocess.model.BPNodeId;
import orchestration.businessprocess.model.BusinessProcessModel;
import orchestration.businessprocess.model.nodes.BPNode;
import orchestration.businessprocess.model.nodes.BPNodesManager;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
 * 
 * a Factory for BPM that reads a .bpmn file (created with the orchestration modeler).
 * @author <a href="mailto:car.ferrarini@gmail.com">Carlo Ferrarini</a>
 *
 */
public class BpmXmlFactory {
	/**
	 * 
	 * @param is the .bpmn file to parse
	 * @return BusinessProcessModel the model parsed
	 * @throws BpmParsingException if some error occurred while parsing
	 */
	public static BusinessProcessModel getBPM(InputStream is) throws BpmParsingException{
		DocumentBuilder builder;
		HashMap<BPNodeId, BPNode> bpnodes= new HashMap<BPNodeId, BPNode>();
		HashMap<BPArtifactId, BPArtifact> bpartifacts= new HashMap<BPArtifactId, BPArtifact>();
		//maps used to concat nodes
		
		Map<String,String> edge2src= new HashMap<String,String>();
		Map<String,String> edge2trt= new HashMap<String, String>();
		//maps for association between nodes and artifacts
		Map<String,String> ass2InputArt= new HashMap<String, String>();
		Map<String,String> ass2OutputArt= new HashMap<String, String>();
		
			try {
				builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			Document doc = builder.parse(is);
			
			//create artifacts, fill maps for association with nodes
			createBPArtifacts(doc,bpartifacts,ass2InputArt,ass2OutputArt);
			//create BPNodes, fill maps for concatenation and attach artifacts
			BPNodeId start = createBPNodesAndFindStart(doc, bpnodes,edge2src,edge2trt,ass2InputArt,ass2OutputArt,bpartifacts);
			//visit edges part
			concatNodes(doc, bpnodes,edge2src,edge2trt);
				return new BPMImpl(start, bpnodes,bpartifacts);
			} catch (ParserConfigurationException e) {
				throw new BpmParsingException(e.getMessage());
			} catch (FactoryConfigurationError e) {
				throw new BpmParsingException(e.getMessage());
			} catch (SAXException e) {
				throw new BpmParsingException(e.getMessage());
			} catch (IOException e) {
				throw new BpmParsingException(e.getMessage());

			}
	}
	/**
	 * @param doc the .bpmn file to parse, here as a DOM Document
	 * @param bpartifacts where to add the parsed Artifacts
	 * @param ass2OutputArt 
	 * @param ass2InputArt 
	 * @throws BpmParsingException if some error occurred reading association direction
	 */
	private static void createBPArtifacts(Document doc,
			HashMap<BPArtifactId, BPArtifact> bpartifacts, Map<String, String> ass2InputArt, Map<String, String> ass2OutputArt) throws BpmParsingException {
		NodeList dataObjs = doc.getElementsByTagName("artifacts");
		int dataObjSize = dataObjs.getLength();
		for(int k=0;k<dataObjSize;k++){
			Node a = dataObjs.item(k);
			NamedNodeMap artifactAttrs = a.getAttributes();
			String aId= parseId(artifactAttrs);
			Node namedItem = artifactAttrs.getNamedItem("name");
			String name=namedItem==null?"":namedItem.getNodeValue();
			Class<?> artType=null;
			Class<?> artComponentType=null;
			BPArtifactId bpaid= new BPArtifactId(aId);
			NodeList assNodes= a.getChildNodes();
			int assNodeSize = assNodes.getLength();
			for(int l=0; l<assNodeSize;l++){
				Node child= assNodes.item(l);	
				String childNodeName = child.getNodeName();
				if(childNodeName.equals("associations")){
					NamedNodeMap aAttrs= child.getAttributes();
					String assId = parseId(aAttrs);
					String direction = aAttrs.getNamedItem("direction").getNodeValue();
					if(direction.equals("To"))
						ass2OutputArt.put(assId, aId);
					else if(direction.equals("From"))
						ass2InputArt.put(assId, aId);
					else if(direction.equals("Both")){
						ass2InputArt.put(assId, aId);
						ass2OutputArt.put(assId, aId);
					}else
						throw new BpmParsingException("Artifact "+ aId + "has association"+ assId + "with direction "+direction);
				}else if(childNodeName.equals("eAnnotations")){
					try {
						String className =BPAnnotationXmlFactory.parseAnnotation(child).getDetail("class");
						if(className!=null){
							//check generics
							int startGenericDeclaration = className.indexOf("<");
							if(startGenericDeclaration==-1){//no generics
								artType= Class.forName(className);
							}else{
								//need to handle generics. creates list (other type of generics are not supported)
								artType=Class.forName(className.substring(0, startGenericDeclaration));
								if(List.class.isAssignableFrom(artType))
									artComponentType=Class.forName(className.substring(startGenericDeclaration+1, className.length()-1));
								else
									throw new BpmParsingException("Only generic lists are supported");
							}
						}
					} catch (UnexpectedNodeException e) {
						throw new BpmParsingException(e.getMessage());
					} catch (ClassNotFoundException e) {
						throw new BpmParsingException(e.getMessage());
					}
				}
			}

			BPArtifact artifact= new ParcelableArtifact(bpaid,name,artType,artComponentType);
			bpartifacts.put(bpaid, artifact);
		}
	}

	/**
	 * concats nodes eventually adding an expression to check at execution time
	 * note that the execution algorithm takes in account expressions only if 
	 * the edge's source is an ExGateway 
	 * @param doc  the .bpmn file to parse, here as a DOM Document
	 * @param bpnodes the map containing the parsed nodes
	 * @param edge2trt 
	 * @param edge2src 
	 */
	private static void concatNodes(Document doc, HashMap<BPNodeId, BPNode> bpnodes, Map<String, String> edge2src, Map<String, String> edge2trt) {
		//map for association with conditions
		Map<String,String> edge2expr= new HashMap<String, String>();

		NodeList edges = doc.getElementsByTagName("sequenceEdges");
		for(int j=0; j< edges.getLength();j++){
			Node e = edges.item(j);
			
			NamedNodeMap eAttrs= e.getAttributes();
			Node expr = eAttrs.getNamedItem("name");
			String id = eAttrs.getNamedItem("xmi:id").getNodeValue();
			if(expr!=null)
				edge2expr.put(id, expr.getNodeValue());
			Node isDefault = eAttrs.getNamedItem("isDefault");
			if(isDefault!=null && isDefault.getNodeValue().equals("true"))
				edge2expr.put(id, "DEFAULT");
		}
		//concat nodes
		for(String edge : edge2src.keySet()){
			BPNodeId srcId= new BPNodeId(edge2src.get(edge));
			BPNodeId trtId= new BPNodeId(edge2trt.get(edge));
			BPNodesManager.concat(bpnodes.get(srcId), bpnodes.get(trtId), edge2expr.get(edge));
		}
	}

	/**
	 * 
	 * @param doc
	 * @param bpnodes
	 * @param edge2trt 
	 * @param edge2src 
	 * @param ass2OutputArt 
	 * @param ass2InputArt 
	 * @param bpartifacts 
	 * @return the id of the StartEvent (first node to visit during an execution)
	 * @throws BpmParsingException
	 */
	private static BPNodeId createBPNodesAndFindStart(Document doc,
			HashMap<BPNodeId, BPNode> bpnodes, Map<String, String> edge2src, Map<String, String> edge2trt, Map<String, String> ass2InputArt, Map<String, String> ass2OutputArt, Map<BPArtifactId, BPArtifact> bpartifacts) throws BpmParsingException {
		BPNodeId start=null;
		NodeList vertices = doc.getElementsByTagName("vertices");
		for(int i=0;i<vertices.getLength();i++){
			Node v=vertices.item(i);
			NamedNodeMap vAttrs =v.getAttributes();
			String id=parseId(vAttrs);
			String outgoingEdges= readOutgoingEdges(vAttrs);
			String incomingEdges=readIncomingEdges(vAttrs);
			if(outgoingEdges!=null)
				for(String out : outgoingEdges.split(" "))
					edge2src.put(out, id);
			if(incomingEdges!=null)
				for(String inc : incomingEdges.split(" "))
					edge2trt.put(inc, id);
			BPNode n= createNode(v,ass2InputArt,ass2OutputArt,bpartifacts);
			if(n.getNodeType()==BPNode.NODE_TYPE_ST_EV)
				start=n.getId();
			bpnodes.put(n.getId(), n);
		}
		return start;
	}
	/**
	 * parses a single node, eventually attaching his annotations
	 * @param node
	 * @param ass2OutputArt 
	 * @param ass2InputArt 
	 * @param bpartifacts 
	 * @return
	 * @throws BpmParsingException
	 */
	public static BPNode createNode(Node node, Map<String, String> ass2InputArt, Map<String, String> ass2OutputArt, Map<BPArtifactId, BPArtifact> bpartifacts) throws BpmParsingException {
			NamedNodeMap attributes = node.getAttributes();
			String type= readNodeType(attributes);
			String name= readNodeName(attributes);
			BPNodeId id= new BPNodeId(parseId(attributes));
			Set<BPArtifact> input= new HashSet<BPArtifact>();
			Set<BPArtifact> output= new HashSet<BPArtifact>();
			parseInputOutput(attributes,input,output,ass2InputArt,ass2OutputArt,bpartifacts);
			BPNode result;
			//create the node
			if(type.equals("Task") || type.equals("SubProcess")){ //treating SubProcesses as Tasks
				result= BPNodesManager.createBPTask(id,input,output);
			}else if(type.equals("EventStartEmpty")){
				result= BPNodesManager.createBPStartEvent(id,output);
			}else if(type.equals("EventEndEmpty")){
				result= BPNodesManager.createBPEndEvent(id,input);
			}else if(type.equals("EventIntermediateEmpty")){
				result= BPNodesManager.createBPIntEvent(id,input,output);
			}else if(type.equals("GatewayParallel")){
				if(readIncomingEdges(attributes).split(" ").length>1)
					result= BPNodesManager.createBPJoinGateway(id);
				else
					result= BPNodesManager.createBPForkGateway(id);
			}else if(type.equals("GatewayDataBasedExclusive"))
				if(readIncomingEdges(attributes).split(" ").length>1)
					result= BPNodesManager.createBPMergeGateway(id);
				else
					result= BPNodesManager.createBPExGateway(id,input);
			else
				throw new BpmParsingException("Unrecognized node "+id.getValue());
			//set the name
			result.setName(name);
			//attach annotations
			NodeList childNodes= node.getChildNodes();
			for(int i=0;i<childNodes.getLength();i++){
				Node child= childNodes.item(i);
				if(child.getNodeName().equals("eAnnotations")){
					try {
						result.attachAnnotation(BPAnnotationXmlFactory.parseAnnotation(child));
					} catch (UnexpectedNodeException e) {
						throw new BpmParsingException(e.getMessage());
					}
				}
			}
			return result;
	}
	/**
	 * 
	 * @param attributes
	 * @param input
	 * @param output
	 * @param ass2InputArt
	 * @param ass2OutputArt
	 */
	private static void parseInputOutput(NamedNodeMap attributes,Set<BPArtifact>input,Set<BPArtifact> output, Map<String, String> ass2InputArt, Map<String, String> ass2OutputArt,Map<BPArtifactId,BPArtifact> bpartifacts) {
		Node assocs = attributes.getNamedItem("associations");
		if(assocs!=null)
			for(String ass : assocs.getNodeValue().split(" ")){
				String inputAidValue=ass2InputArt.get(ass);
				if(inputAidValue!=null)
					input.add(bpartifacts.get(new BPArtifactId(inputAidValue)));
				String outputAidValue= ass2OutputArt.get(ass);
				if(outputAidValue!=null)
					output.add(bpartifacts.get(new BPArtifactId(outputAidValue)));
			}
	}

	private static String readIncomingEdges(NamedNodeMap attributes) {
		Node incomingEdgesNode = attributes.getNamedItem("incomingEdges");
		if(incomingEdgesNode!=null)
			return incomingEdgesNode.getNodeValue();
		return null;
	}
	private static String readOutgoingEdges(NamedNodeMap attributes) {
		Node outgoingEdgesNodes = attributes.getNamedItem("outgoingEdges");
		if(outgoingEdgesNodes!=null)
			return outgoingEdgesNodes.getNodeValue();
		return null;
	}
	private static String readNodeType(NamedNodeMap attributes) {
		String nodeType;
		Node xmiType = attributes.getNamedItem("xmi:type");
		Node activityNode = attributes.getNamedItem("activityType");
		if(xmiType!=null && xmiType.getNodeValue().equals("bpmn:SubProcess"))
			nodeType="SubProcess";
		else if(activityNode==null) //bug in the modeler plugin!
			nodeType="Task";
		else
			nodeType= activityNode.getNodeValue();
		return nodeType;
	}
	private static String readNodeName(NamedNodeMap attributes) {
		Node nameNode = attributes.getNamedItem("name");
		if(nameNode==null)
			return "";
		return nameNode.getNodeValue();
	}
	
	private static String parseId(NamedNodeMap attributes) {
		return attributes.getNamedItem("xmi:id").getNodeValue();
	}
}
